Skip to content

Feature | Add Login UI MFA Flow#142

Open
matiasperrone-exo wants to merge 1 commit into
feat/mfa---usercontroller-mfa-integration-device-trust-cookie-management-audit-wiring-and-2fa-rate-limiting---cu-86ba2zc6pfrom
feat/mfa---login-ui-flow
Open

Feature | Add Login UI MFA Flow#142
matiasperrone-exo wants to merge 1 commit into
feat/mfa---usercontroller-mfa-integration-device-trust-cookie-management-audit-wiring-and-2fa-rate-limiting---cu-86ba2zc6pfrom
feat/mfa---login-ui-flow

Conversation

@matiasperrone-exo

@matiasperrone-exo matiasperrone-exo commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Task:

Ref: https://app.clickup.com/t/9014802374/86ba2zp3q

Changes

Preview

image

Overview

Frontend-only change adding a multi-factor authentication (MFA) flow to the React login UI. The monolithic login.js was refactored — inline form components were extracted into dedicated files, and two new MFA screens (verification code + recovery code) were added.

Highlights

  • New MFA flow: after a successful password step, the backend can signal mfa_required, which transitions the UI into a two-factor email-OTP challenge instead of completing login.
  • Redirect-aware AJAX: a new postRawRequestFull helper captures the final URL after server-side redirects (xhr.responseURL) so the SPA can navigate the top window to the post-login destination.
  • Component extraction: all inline form definitions in login.js were moved into resources/js/login/components/, slimming and reorganizing the main file.

New Files

Actions & constants

  • resources/js/login/constants.jsHTTP_CODES, LOGIN_FLOW, MFA_METHODS, FLOW, MFA_ERROR_CODE, and defaults (OTP length 6, TTL 300, default method email_otp).

Login form components (resources/js/login/components/)

  • two_factor_form.js — MFA email-OTP entry with expiry countdown, 30s resend cooldown, "Trust this device for 30 days", links to recovery code / cancel.
  • recovery_code_form.js — recovery code entry screen with back-to-OTP and cancel.
  • email_input_form.js — extracted email entry form.
  • password_input_form.js — extracted password form (incl. failed-attempt / account-lock messaging).
  • otp_input_form.js — extracted single-use code form.
  • email_error_actions.js, existing_account_actions.js, help_links.js, otp_help_links.js, third_party_identity_providers.js — extracted helper/link components.

Modified Files

  • resources/js/base_actions.js — added postRawRequestFull (returns { response, status, finalUrl }).
  • resources/js/login/actions.js — added verify2FA, resend2FA, verifyRecoveryCode, and authenticateWithPassword (uses the redirect-aware helper).
  • resources/js/login/login.js — major refactor:
    • Imports extracted components instead of defining them inline.
    • Added MFA state (mfaMethod, trustDevice, twoFactorCode, recoveryCode, otpLength, otpLifetime, codeVersion) and handlers (onVerify2FA, onResend2FA,
      onVerifyRecovery, onCancel2FA, onUseRecovery, onBackToOtp).
    • Password auth now goes through authenticateWithPassword; on mfa_required it switches to the MFA flow, otherwise redirects via finalUrl.
    • Render logic gated by derived flow flags (showTwoFactorForm, showRecoveryForm, isPasswordFlow, isOtpFlow, showDefaultFlow).
  • resources/js/login/login.module.scss — new styles: .info_message, .countdown, .trust_device_row, .disabled_link.
  • resources/views/auth/login.blade.php — wired new endpoints/config: verify2faAction, resend2faAction, recovery2faAction, mfaMethod, plus session-driven otpLength /
    otpLifetime; exposes VERIFY_2FA_ENDPOINT, RESEND_2FA_ENDPOINT, RECOVERY_2FA_ENDPOINT, FORM_ACTION_ENDPOINT on window.
  • package.json — updated serve script flag (--https--server-type https) for the newer webpack-dev-server.

User's GOAL

Current state

The login React component supports password and passwordless OTP flows. It has no MFA verification screen after password authentication.

Target state

The login UI supports a third flow state, '2fa'. When the backend returns mfa_required, the UI renders an MFA verification step with OTP input, trust-device checkbox, resend action, recovery-code link, and relevant error handling.

TASKS

  • refactor all reactjs components from resources/js/login/login.js and move then to a new folder under resources/js/login/components
    ( each component should have its own file )
  • Add '2fa' flow state to login component.
  • Detect error_code=mfa_required in login response.
  • Store username, otp_length, otp_lifetime, and method from response.
  • Render informational message: "A verification code has been sent to your email".
  • Reuse existing OTP input component.
  • Render "Trust this device for 30 days" checkbox, unchecked by default.
  • Implement OTP submit to POST /auth/login/2fa/verify with:
    • otp_value
    • method='email_otp'
    • trust_device
  • Implement resend to POST /auth/login/2fa/resend with method='email_otp'.
  • Handle resend success with user feedback and cooldown/countdown.
  • Handle 429 response with rate-limit message.
  • Implement Cancel action to return to password screen and clear client-side MFA state.
  • Add "Use a recovery code instead" link that transitions to the recovery-code login mode implemented in Ticket 12A.
  • Add countdown timer based on otp_lifetime.
  • Hide "Use a different method" in Phase I.
  • Ensure CSRF token is included in all POST requests.

ACCEPTANCE CRITERIA

  • mfa_required response transitions UI to 2FA screen.
  • OTP input length follows otp_length from server.
  • Valid OTP completes login and follows normal redirect/OIDC flow.
  • Invalid OTP shows inline error and remains on 2FA screen.
  • mfa_session_expired shows message and returns user to password flow.
  • mfa_rate_limit shows "Too many attempts" or approved product copy.
  • Resend calls correct endpoint and shows feedback.
  • Cancel returns to password flow.
  • Existing password and passwordless OTP flows are unaffected.

DEVELOPMENT NOTES

Key files:

  • Modified: resources/js/login/login.js
  • Possibly modified: resources/views/auth/login.blade.php
  • Existing: react-otp-input or equivalent OTP component

Gotchas:

  • The backend may return mfa_required through login_strategy->errorLogin(), which may be represented as flash data or JSON depending on flow. Follow existing login error handling.
  • Keep this as a state in the existing login component, not a separate page.
  • Trust-device checkbox must default to unchecked.

Risks:

  • Risk: React flow does not receive enough data from backend. Mitigation: verify mfa_required response includes username, method, otp_length, otp_lifetime.

Out of scope:

Recovery-code profile management, device-management UI, method selector for future MFA methods.

Summary by CodeRabbit

  • New Features

    • Added two-factor authentication (2FA) with one-time password (OTP) verification.
    • Added recovery code verification option.
    • Added "trust this device" functionality to streamline future logins.
    • Added support for third-party identity providers for social login.
    • Added CAPTCHA integration for enhanced security.
    • Improved login flow with better error messaging and code expiry countdowns.
  • Bug Fixes

    • Improved session expiration handling during authentication.
  • Chores

    • Updated HTTPS server configuration.

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: adding an MFA flow to the login UI.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mfa---login-ui-flow

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@matiasperrone-exo matiasperrone-exo self-assigned this Jun 22, 2026
@github-actions

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-142/

This page is automatically updated on each push to this PR.

@github-actions

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-142/

This page is automatically updated on each push to this PR.

@github-actions

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-142/

This page is automatically updated on each push to this PR.

@matiasperrone-exo matiasperrone-exo marked this pull request as ready for review June 23, 2026 14:28
@matiasperrone-exo matiasperrone-exo self-assigned this Jun 23, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@resources/js/base_actions.js`:
- Around line 102-106: The issue is that params are unconditionally being added
to the URL query string via the url.query(params) call, which exposes sensitive
POST data like passwords, OTPs, and captchas in URLs and logs. Modify the code
to only add params to the query string for non-POST requests. Check the HTTP
method of the request before calling url.query(params) and only apply it when
the method is not POST, ensuring sensitive data in POST requests stays in the
request body and is never exposed in the URL.

In `@resources/js/login/components/email_error_actions.js`:
- Around line 22-42: The two Button components in this block are not respecting
the disableInput state, allowing users to trigger actions even when the UI
should be locked. Add the disabled prop set to disableInput on both buttons: the
button with onClick={emitOtpAction} and the button with
href={createAccountAction}. This will prevent users from clicking these recovery
action buttons when the input is disabled.

In `@resources/js/login/components/email_input_form.js`:
- Around line 51-55: Remove the use of dangerouslySetInnerHTML on the error
message element in the email error rendering section. Instead of using
dangerouslySetInnerHTML with the emailError property, render the emailError text
directly as a child of the paragraph element to prevent potential XSS
vulnerabilities. This applies to the conditional block where emailError is not
empty and the error paragraph is being displayed.

In `@resources/js/login/components/existing_account_actions.js`:
- Around line 34-37: The `Link` component with `disabled={disableInput}` prop
does not actually prevent navigation in MUI v4 when using the default anchor
element. To fix this, add an onClick handler to the Link component that prevents
the default navigation behavior when disableInput is true. The onClick handler
should call preventDefault() on the event whenever disableInput is true,
effectively blocking the navigation while the input is locked.

In `@resources/js/login/components/help_links.js`:
- Around line 18-21: The URL construction for forgotPasswordActionHref does not
account for existing query parameters in the forgotPasswordAction base URL. When
appending the email parameter, the code always uses `?email=...`, which will
create invalid URLs if forgotPasswordAction already contains a query string.
Modify the conditional block where forgotPasswordActionHref is assigned (when
userName exists) to check if forgotPasswordAction already contains a `?`
character. If it does, use `&` to append the email parameter; otherwise use `?`.
This ensures proper URL formatting regardless of whether the base URL already
has query parameters.

In `@resources/js/login/components/otp_input_form.js`:
- Around line 55-58: The error label paragraph element in the OTP input form is
using dangerouslySetInnerHTML to render the otpError variable, which creates an
XSS security vulnerability. Remove the dangerouslySetInnerHTML prop and instead
render the otpError directly as text content of the paragraph element so that
any HTML-like characters in the error message are displayed as plain text rather
than being interpreted as executable HTML.
- Line 1: Remove the useMemo hook to prevent stale captcha visibility state.
First, remove useMemo from the import statement at the top of the
otp_input_form.js file. Then, locate where useMemo is being used in the
component (likely wrapping captcha visibility logic) and unwrap it by removing
the useMemo function call while keeping the code it was wrapping. This ensures
the captcha visibility logic runs on every render and properly reflects the
current state of dependencies like loginAttempts, rather than relying on a stale
memoized result based only on the function reference.
- Line 49: The `hasErrored` prop being passed to the OtpInput component is not
supported in react-otp-input v3.1.1 and was removed in v3.0.0+. Remove the
`hasErrored={!otpError}` prop from the OtpInput component and instead use the
`renderInput` prop to handle error state styling. Pass the otpError state to the
custom input component through the `renderInput` prop so that the error styling
can be conditionally applied within the input component based on the parent
component's otpError state.

In `@resources/js/login/components/password_input_form.js`:
- Around line 85-88: The paragraph element with className styles.error_label is
rendering passwordError as HTML using dangerouslySetInnerHTML, which creates an
XSS vulnerability. Remove the dangerouslySetInnerHTML prop and instead render
passwordError directly as text content within the paragraph element to safely
display the error message without executing any embedded HTML or scripts.

In `@resources/js/login/components/recovery_code_form.js`:
- Around line 22-25: The handleBack function defined in the recovery_code_form
component is not connected to any UI element, so users cannot interact with it
to go back to the OTP screen. Add a button element in the form's JSX render that
calls the handleBack function on click. This button should be placed in a
logical location in the recovery form UI, likely near the form submission or as
a navigation element. Note that the same issue applies to the code around lines
65-72, so ensure both instances have the corresponding UI elements added to
enable the back navigation functionality.
- Around line 53-55: The recoveryError is being rendered as raw HTML using
dangerouslySetInnerHTML, which creates an XSS vulnerability if the error content
contains user-controlled or backend text. Replace the dangerouslySetInnerHTML
prop with direct text rendering by removing the
dangerouslySetInnerHTML={{__html: recoveryError}} attribute and instead render
recoveryError directly as plain text inside the paragraph element with the
error_label className. This will automatically escape any HTML special
characters and prevent potential XSS attacks.

In `@resources/js/login/components/two_factor_form.js`:
- Around line 91-93: The otpError variable is being rendered with
dangerouslySetInnerHTML which creates an XSS vulnerability since the value comes
from server responses. Remove the dangerouslySetInnerHTML attribute from the
error message paragraph element in the two_factor_form component and instead
render otpError as plain text content. If HTML formatting is required for the
error message, sanitize otpError using a proper HTML sanitization library before
using dangerouslySetInnerHTML.

In `@resources/js/login/login.js`:
- Around line 435-463: Add an early guard clause at the beginning of the
onVerify2FA method to prevent duplicate verification requests. Before processing
any validation logic or making the verify2FA API call, check if
this.state.disableInput is already true, and if so, return early from the
method. This will prevent rapid successive clicks or Enter key events from
triggering multiple concurrent API calls. Apply the same guard pattern to the
other verify handler mentioned in the comment (around lines 524-547).
- Around line 183-202: In the handleAuthenticateValidation method, the password
validation block that checks if user_password is empty is being executed
unconditionally for all auth flows. This causes the OTP flow validation to fail
even when a valid OTP code is provided, because the password check comes after
the OTP check and always requires a password. Wrap the password validation block
in a conditional statement that only executes when the auth flow is NOT
FLOW.OTP, so that OTP flow validation can complete successfully without
requiring a password field.
- Around line 220-241: The `default` case in the switch statement declares
`const redirect` without block scope, creating scope and TDZ issues. Wrap the
entire contents of the `default` case (from the `const redirect` declaration
through the closing of the if-else block that checks the redirect condition)
with curly braces to create proper block scope and isolate the variable
declaration from other switch cases.

In `@resources/views/auth/login.blade.php`:
- Around line 102-107: The line setting window.RESET_2FA_ENDPOINT is referencing
config.reset2faAction which is not defined in the template's configuration
object, causing the window variable to become undefined. Verify the template's
PHP configuration section where the config object is initialized and either add
the missing reset2faAction property with the appropriate endpoint URL, or
correct the reference to use the correct existing config key name that
corresponds to the reset 2FA endpoint.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dd2029b0-3aab-49c1-b3e1-2abdc066795e

📥 Commits

Reviewing files that changed from the base of the PR and between 5fc47a4 and 3481ac9.

📒 Files selected for processing (17)
  • package.json
  • resources/js/base_actions.js
  • resources/js/login/actions.js
  • resources/js/login/components/email_error_actions.js
  • resources/js/login/components/email_input_form.js
  • resources/js/login/components/existing_account_actions.js
  • resources/js/login/components/help_links.js
  • resources/js/login/components/otp_help_links.js
  • resources/js/login/components/otp_input_form.js
  • resources/js/login/components/password_input_form.js
  • resources/js/login/components/recovery_code_form.js
  • resources/js/login/components/third_party_identity_providers.js
  • resources/js/login/components/two_factor_form.js
  • resources/js/login/constants.js
  • resources/js/login/login.js
  • resources/js/login/login.module.scss
  • resources/views/auth/login.blade.php

Comment thread resources/js/base_actions.js Outdated
Comment thread resources/js/login/components/email_error_actions.js
Comment thread resources/js/login/components/email_input_form.js Outdated
Comment thread resources/js/login/components/existing_account_actions.js
Comment thread resources/js/login/components/help_links.js
Comment thread resources/js/login/components/two_factor_form.js
Comment thread resources/js/login/login.js Outdated
Comment thread resources/js/login/login.js
Comment thread resources/js/login/login.js
Comment thread resources/views/auth/login.blade.php
@github-actions

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-142/

This page is automatically updated on each push to this PR.

3 similar comments
@github-actions

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-142/

This page is automatically updated on each push to this PR.

@github-actions

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-142/

This page is automatically updated on each push to this PR.

@github-actions

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-142/

This page is automatically updated on each push to this PR.

@matiasperrone-exo matiasperrone-exo force-pushed the feat/mfa---login-ui-flow branch from c54e492 to e0b8519 Compare June 24, 2026 18:25
@github-actions

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-142/

This page is automatically updated on each push to this PR.

@matiasperrone-exo matiasperrone-exo changed the title feat: Add Login UI MFA Flow Feature | Add Login UI MFA Flow Jun 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants